﻿/*
 *   Alu.cs
 *   Copyright (c) 2011 - BitFox Software and Ricardo Federico Villafañes
 *   Contact: federvillaf@hotmail.com
 *   
 *   Licensing: This product is covered by Microsoft Reciprocal License (MS-RL)
 *              see https://bitfoxeval.codeplex.com/license
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;

namespace BitFox.Evaluator
{
    /// <summary>
    /// Class with helpers used during execution of a compiled expression
    /// </summary>
    class Alu
    {
        /// <summary>
        /// Helper method that 'elevates' a number to a higher type. It's used when an arithmetic operation is
        /// performed and it throws 'overflow'
        /// </summary>
        /// <param name="o">The object to elevate</param>
        /// <returns>The elevated object</returns>
        /// <exception cref="BitFoxException">If something was wrong during the convertion</exception>
        static object Elevate(object o)
        {
            try
            {
                if (o is Int64)
                    return FoxConvert.ToDecimal(o);
                else if (o is Int32)
                    return FoxConvert.ToInt64(o);
                else
                    return FoxConvert.ToDecimal(o);
            }
            catch (Exception e)
            {
                throw new BitFoxException(ErrorCode.CannotConvertToNumber, e);
            }
        }

        /// <summary>
        /// Automatically promotes an object to the type of other.
        /// </summary>
        /// <param name="o1">The first argument</param>
        /// <param name="o2">The second argument</param>
        /// <remarks>
        /// Which of arguments is promoted depends upon wich of both is of the 'lower' type. The automatic
        /// promotions did by this method are, basically, numeric promotions. In these promotions, integers
        /// of lower number of bits are promoted to higher number of bits, integers are promoted to decimals
        /// and doubles are promoted to decimal too.
        /// </remarks>
        static void Promote(ref object o1, ref object o2)
        {
            TypeCode t1 = Type.GetTypeCode(o1.GetType());
            TypeCode t2 = Type.GetTypeCode(o2.GetType());

            try
            {
                if (t1 == TypeCode.Decimal || t2 == TypeCode.Decimal || t1 == TypeCode.Double || t2 == TypeCode.Double)
                {
                    if (!(o1 is decimal))
                        o1 = FoxConvert.ChangeType(o1, typeof(decimal));
                    if (!(o2 is decimal))
                        o2 = FoxConvert.ChangeType(o2, typeof(decimal));
                }
                else if (t1 == TypeCode.Int64 || t2 == TypeCode.Int64)
                {
                    if (!(o1 is long))
                        o1 = FoxConvert.ChangeType(o1, typeof(long));
                    if (!(o2 is long))
                        o2 = FoxConvert.ChangeType(o2, typeof(long));
                }
                else if (t1 == TypeCode.Int32 || t2 == TypeCode.Int32)
                {
                    if (!(o1 is int))
                        o1 = FoxConvert.ChangeType(o1, typeof(int));
                    if (!(o2 is int))
                        o2 = FoxConvert.ChangeType(o2, typeof(int));
                }
                else if (t1 == TypeCode.Int16 || t2 == TypeCode.Int16)
                {
                    if (!(o1 is short))
                        o1 = FoxConvert.ChangeType(o1, typeof(short));
                    if (!(o2 is short))
                        o2 = FoxConvert.ChangeType(o2, typeof(short));
                }
                else if (t1 == TypeCode.String || t2 == TypeCode.String)
                {
                    if (!(o1 is string))
                        o1 = o1.ToString();
                    if (!(o2 is string))
                        o2 = o2.ToString();
                }
            }
            catch (BitFoxException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new BitFoxException(ErrorCode.CannotPromoteValues, e);
            }
        }

        /// <summary>
        /// This helper method compares two objects, discovering the type of each one and doing the appropiated 
        /// comparison.
        /// </summary>
        /// <param name="o1">The first object to compare</param>
        /// <param name="o2">The second object to compare</param>
        /// <returns>-1 if o1 is lower than o2, 1 if they are equal, and 1 if o1 is higher than o2</returns>
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public int Compare(object o1, object o2)
        {
            Promote(ref o1, ref o2);
            try
            {
                switch (Type.GetTypeCode(o1.GetType()))
                {
                    case TypeCode.Byte:
                        return ((byte)o1).CompareTo(Convert.ToByte(o2, CultureInfo.InvariantCulture));

                    case TypeCode.Int16:
                        return ((Int16)o1).CompareTo(FoxConvert.ToInt16(o2));

                    case TypeCode.Int32:
                        return ((Int32)o1).CompareTo(FoxConvert.ToInt32(o2));

                    case TypeCode.Int64:
                        return ((Int64)o1).CompareTo(FoxConvert.ToInt64(o2));

                    case TypeCode.Decimal:
                        return ((Decimal)o1).CompareTo(FoxConvert.ToDecimal(o2));

                    case TypeCode.DateTime:
                        return ((DateTime)o1).CompareTo(Convert.ToDateTime(o2, CultureInfo.InvariantCulture));

                    case TypeCode.String:
                        return string.Compare(
                            (string)o1, 
                            Convert.ToString(o2, CultureInfo.InvariantCulture),
                            StringComparison.Ordinal);

                    case TypeCode.Boolean:
                        {
                            bool b1 = (bool)o1, b2 = (bool)o2;
                            if (b1 && b2)                           // .T. == .T.
                                return 0;
                            if (!b1 && !b2)                         // .F. == .F.
                                return 0;
                            if (b1 && !b2)                          // .T. > .F.
                                return 1;
                            return -1;                              // .F. < .T.
                        }

                    default:
                        throw new BitFoxException(ErrorCode.CannotCompareValues);
                }
            }
            catch (BitFoxException)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new BitFoxException(ErrorCode.CannotCompareValues, e);
            }
        }

        /// <summary>
        /// Do the add operation. (For strings, 'add' is defined as concatenation).
        /// </summary>
        /// <param name="o1">The first operand</param>
        /// <param name="o2">The second operand</param>
        /// <returns>The result of adding <paramref name="o2"/> to <paramref name="o1"/></returns>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Add(object o1, object o2)
        {
            while (true)
            {
                // 'Equalize' type of both arguments
                Promote(ref o1, ref o2);
                try
                {
                    switch (Type.GetTypeCode(o1.GetType()))
                    {
                        case TypeCode.Byte:
                            return checked(((byte)o1) + ((byte)o2));

                        case TypeCode.Int16:
                            return checked(((Int16)o1) + ((Int16)o2));

                        case TypeCode.Int32:
                            return checked(((Int32)o1) + ((Int32)o2));

                        case TypeCode.Int64:
                            return checked(((Int64)o1) + ((Int64)o2));

                        case TypeCode.Decimal:
                            return checked(((Decimal)o1) + ((Decimal)o2));

                        case TypeCode.String:
                            return checked(((String)o1) + ((String)o2));

                        default:
                            throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for adding");
                    }
                }
                catch (OverflowException)
                {
                    if (o1 is decimal || o1 is string) throw;
                    o1 = Elevate(o1);
                }
            }
        }

        /// <summary>
        /// Do a substraction between two objects. For strings, a substraction is defined as a concatenation of trimmed input.
        /// </summary>
        /// <param name="o1">The first operand</param>
        /// <param name="o2">The second operand</param>
        /// <returns>The result of substract <paramref name="o2"/> to <paramref name="o1"/></returns>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Substract(object o1, object o2)
        {
            while (true)
            {
                Promote(ref o1, ref o2);
                try
                {
                    switch (Type.GetTypeCode(o1.GetType()))
                    {
                        case TypeCode.Byte:
                            return checked(((byte)o1) - ((byte)o2));

                        case TypeCode.Int16:
                            return checked(((Int16)o1) - ((Int16)o2));

                        case TypeCode.Int32:
                            return checked(((Int32)o1) - ((Int32)o2));

                        case TypeCode.Int64:
                            return checked(((Int64)o1) - ((Int64)o2));

                        case TypeCode.Decimal:
                            return checked(((Decimal)o1) - ((Decimal)o2));

                        case TypeCode.String:
                            return checked(((String)o1).Trim() + ((String)o2).Trim());

                        default:
                            throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for substracting");
                    }
                }
                catch (OverflowException)
                {
                    if (o1 is decimal || o1 is string) throw;
                    o1 = Elevate(o1);
                }
            }
        }

        /// <summary>
        /// Do multiplication of two objects
        /// </summary>
        /// <param name="o1">The first operand</param>
        /// <param name="o2">The second operand</param>
        /// <returns>The result of multiply <paramref name="o1"/> to <paramref name="o2"/></returns>
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Multiply(object o1, object o2)
        {
            while (true)
            {
                Promote(ref o1, ref o2);
                try
                {
                    switch (Type.GetTypeCode(o1.GetType()))
                    {
                        case TypeCode.Byte:
                            return checked(((byte)o1) * ((byte)o2));

                        case TypeCode.Int16:
                            return checked(((Int16)o1) * ((Int16)o2));

                        case TypeCode.Int32:
                            return checked(((Int32)o1) * ((Int32)o2));

                        case TypeCode.Int64:
                            return checked(((Int64)o1) * ((Int64)o2));

                        case TypeCode.Decimal:
                            return checked(((Decimal)o1) * ((Decimal)o2));

                        default:
                            throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for multiplication");
                    }
                }
                catch (OverflowException)
                {
                    if (o1 is decimal || o1 is string) throw;
                    o1 = Elevate(o1);
                }
            }
        }

        /// <summary>
        /// Do division of two objects.
        /// </summary>
        /// <param name="o1">The first operand</param>
        /// <param name="o2">The second operand</param>
        /// <returns>The result of divide <paramref name="o1"/> between <paramref name="o2"/></returns>
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Divide(object o1, object o2)
        {
            while (true)
            {
                Promote(ref o1, ref o2);
                try
                {
                    switch (Type.GetTypeCode(o1.GetType()))
                    {
                        case TypeCode.Byte:
                            return checked(((byte)o1) / ((byte)o2));

                        case TypeCode.Int16:
                            return checked(((Int16)o1) / ((Int16)o2));

                        case TypeCode.Int32:
                            return checked(((Int32)o1) / ((Int32)o2));

                        case TypeCode.Int64:
                            return checked(((Int64)o1) / ((Int64)o2));

                        case TypeCode.Decimal:
                            return checked(((Decimal)o1) / ((Decimal)o2));

                        default:
                            throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for multiplication");
                    }
                }
                catch (OverflowException)
                {
                    if (o1 is decimal || o1 is string) throw;
                    o1 = Elevate(o1);
                }
            }
        }

        /// <summary>
        /// Do the integer modulus of two objects.
        /// </summary>
        /// <param name="o1">The first operand</param>
        /// <param name="o2">The second operand</param>
        /// <returns>The remainder in the division of <paramref name="o1"/> between <paramref name="o2"/></returns>
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Modulus(object o1, object o2)
        {
            while (true)
            {
                Promote(ref o1, ref o2);
                try
                {
                    switch (Type.GetTypeCode(o1.GetType()))
                    {
                        case TypeCode.Byte:
                            return ((byte)o1) % ((byte)o2);

                        case TypeCode.Int16:
                            return ((Int16)o1) % ((Int16)o2);

                        case TypeCode.Int32:
                            return ((Int32)o1) % ((Int32)o2);

                        case TypeCode.Int64:
                            return ((Int64)o1) % ((Int64)o2);

                        default:
                            throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for modulus");
                    }
                }
                catch (OverflowException)
                {
                    if (o1 is decimal || o1 is string || o1 is long) throw;
                    o1 = Elevate(o1);
                }
            }
        }

        /// <summary>
        /// Negates (change the sign) of a given object
        /// </summary>
        /// <param name="o">The argument to negate</param>
        /// <returns>The value of <paramref name="o"/> with changed sign</returns>
        [SuppressMessage("Microsoft.Performance", "CA1822")]
        public object Negate(object o)
        {
            switch (Type.GetTypeCode(o.GetType()))
            {
                case TypeCode.Byte:
                    return -((byte)o);

                case TypeCode.Int16:
                    return -((Int16)o);

                case TypeCode.Int32:
                    return -((Int32)o);

                case TypeCode.Int64:
                    return -((Int64)o);

                case TypeCode.Decimal:
                    return -((Decimal)o);

                default:
                    throw new BitFoxException(ErrorCode.CannotDoDesiredOperation, "Type not supported for negation");
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="context">A context with settings that can change the behavior of some method.</param>
        [SuppressMessage("Microsoft.Usage", "CA1801")]
        public Alu(CalculatorContext context)
        {
        }
    }
}
